強制轉型(coercion)到底是一個有用的功能,還是設計上的缺陷呢?
...
...
本文主要會談到
強制轉型(coercion)分為兩種,分別是「明確的」強制轉型(explicit coercion)和「隱含的」強制轉型(implicit coercion),只要是程式碼中刻意寫出來的型別轉換的動作,就是明確的強制轉型;反之,在程式碼中沒有明確指出要轉換型別卻轉型的,就是隱含的強制轉型。
範例如下,b 的值由運算式 String(a)
而來,這裡表明會將 a 強制轉為字串,因此是明確的強制轉型;而 c 的值由運算式 a + ''
而來,當兩值的型別不同且其中一方是字串時,+
所代表的是字串運算子,要將兩字串做串接,而會將數字強制轉型為字串,並連接兩個字串,因此是隱含的強制轉型,稍後會再詳述。
var a = 42;
var b = String(a); // 明確的強制轉型
var c = a + ''; // 隱含的強制轉型
b // '42'
c // '42'
注意,無論是明確或隱含,強制轉型的結果會是基本型別值,例如:數字、布林或字串。
「抽象的值運算」指的是「內部限定的運算」,意即這是 JavaScript 引擎在背後偷偷幫我們做的工作。在這個部份會來探討 ToString、ToNumber、ToBoolean 和 ToPrimitive 這幾個抽象的值運算,來看看到底在強制轉型時背地裡做了什麼好事。
任何非字串的值被強制轉型為字串時,會遵循 ES5 的規格中的 ToString 來運作。
規則簡單說明如下
'undefined'
。'null'
。'true'
,false -> 'false'
。'1.23e21'
。toString
方法,則會以它自己的 toString
方法所產生的結果為優先,例如,陣列有自己定義的 toString
方法,因此 [1,2,3].toString()
會得到 "1,2,3"
。toString
方法,則回傳內部的屬性 [[Class]]
,這是一個用來標記這個值是屬於物件的哪個子分類的標籤,例如:({}).toString()
會得到 [object Object]
。圖片來源:ToString Conversions
順道一提 JSON 的字串化。
JSON 的字串化 JSON.stringify
將值序列化(serialize)為 JSON 字串,這個轉為 JSON 字串的過程與 ToString 規則有關,但並不等於強制轉型。
規則簡單說明如下
JSON.stringify(42) // "42"
JSON.stringify(true) // "true"
JSON.stringify(null) // "null"
JSON.stringify('Hello World') // ""Hello World"",字串會在外面再包一層引號!
JSON.stringify
會自動忽略這些非法值或丟出錯誤。又,若陣列中某個元素的值為非法值則會自動以 null 取代;若物件中的其中一個屬性為非法值,則會排除這個屬性。toJSON
方法則會優先呼叫此方法,並依此方法之回傳值作為序列化的結果。因此,若試圖 JSON 字串化一個含有非法值的物件,應定義其 toJSON
方法以回傳適當的 JSON-safe 的值。範例如下。
若陣列中某個元素的值為非法值則會自動以 null 取代;若物件中的其中一個屬性為非法值,則會排除這個屬性。
JSON.stringify(undefined) // undefined,忽略非法值
JSON.stringify(function() {}) // undefined,忽略非法值
JSON.stringify(Symbol()) // undefined,忽略非法值
JSON.stringify([1, 2, 3, undefined]) // "[1,2,3,null]",非法值以 null 取代
JSON.stringify({ a: 2, b: function() {}}) // "{"a":2}",忽略非法屬性
具有循環參考的物件,丟出錯誤。
const a = { someProperty: 'Jack' };
const b = { anotherProperty: a };
a.b = b;
JSON.stringify(a) // Uncaught TypeError: Converting circular structure to JSON
JSON.stringify(b) // Uncaught TypeError: Converting circular structure to JSON
針對含有非法值的物件或具有循環參考的物件,解法是定義其 toJSON
方法以回傳 JSON-safe 的值。
範例如下,物件 someObj 含有非法的屬性 b 會導致轉 JSON 字串時被忽略,因此定義其 toJSON
方法只要序列化合法的 a 屬性即可。
const someObj = {
a: 2,
b: function() {}, // 非法!
toJSON: function() {
return {
a: 2, // 序列化過程只包含 a 屬性
}
},
}
JSON.stringify(someObj); // "{"a":2}"
再看一個範例,對於「具有循環參考的物件」該怎麼處理呢?如下,a 和 b 是具有循環參考的物件,在先前的例子中 JSON.stringify(a)
和 JSON.stringify(b)
會丟出錯誤「Uncaught TypeError: Converting circular structure to JSON」,因此分別定義其 toJSON
方法,這裡的序列化過程只包含 prompt 屬性且其值為字串 'Hello World'
。
const a = {
someProperty: 'Jack',
toJSON: function() {
return {
prompt: 'Hello World'
}
},
};
const b = {
anotherProperty: a ,
toJSON: function() {
return {
prompt: 'Hello World'
}
},
};
a.b = b;
// 序列化成功!不會被報錯了!
JSON.stringify(a) // "{"prompt":"Hello World"}"
JSON.stringify(b) // "{"prompt":"Hello World"}"
除了 toJSON
外,JSON.stringify
也可傳入第二個選擇性參數「取代器」(replacer,可為陣列或函式)來自訂過濾機制,決定序列化過程中應該包含哪些屬性。
const someObj = {
a: 2,
b: function() {},
}
JSON.stringify(someObj, ['a']); // "{"a":2}"
const someObj = {
a: 2,
b: function() {},
}
JSON.stringify(someObj, function(key, value) {
if (key !== 'b') {
return value
}
});
// "{"a":2}"
若需要將非數字值當成數字來操作,像是做數學運算,就會遵循 ES5 的規格中的 ToNumber 來運作。
規則簡單說明如下
圖片來源:ToNumber Conversions
範例如下。
Number(undefined) // NaN
Number(null) // 0
Number(true) // 1
Number(false) // 0
Number('12345') // 12345
Number('Hello World') // NaN
Number({ name: 'Jack' }}) // NaN
const a = {
name: 'Apple',
valueOf: function() {
return '999'
}
}
Number(a) // 999
讓我們複習一下 Truthy 與 Falsy 的概念,這會遵循 ES5 的規格中的 ToBoolean 來運作。
在 JavaScript 中會被轉為 false 的值有
""
空字串我們只要熟記這幾個值就可以了! d(d'∀')
而除了以上的值之外,都會被轉為 true,舉例如下
'Hello World'
非空字串[], [1, 2, 3]
陣列,不管是不是空的{}, { name: 'Jack' }
物件,不管是不是空的function foo() {}
函式當使用包裹器物件來建立字串、數字或布林值時,由於包了一層物件,因此就算其底層的基型值是會被轉為 false 的值,它根本上都還是個物件,而只要是物件(即使是空物件),就會被轉為 true。
const a = new String('');
const b = new Number(0);
const c = new Boolean(false);
!!a // true
!!b // true
!!c // true
再次強調,只要不是前面列舉為會轉為 false 的值,都會被轉為 true。
詳細狀況可見 ES5 規格。
規則簡單說明如下
[[DefaultValue]]
內部方法,依照傳入的參數來決定要使用 toString 或 valueOf 取得基本型別值,看參考規格。「明確的強制轉型」是指程式碼中刻意寫出來的明顯的型別轉換的動作。
字串與數字間的明確的強制轉換。
String(..)
與 Number(..)
String(123) // "123"
Number('123') // 123
注意,這裡的 String(..)
是直接調用 .toString
來轉字串,與 +
字串運算子經過 ToPrimitive 的運作-由於傳入 [[DefaultValue]]
演算法的參數是 number,因此先使用 valueOf
取得基型值,然後再用 toString
轉為字串,兩種方法是完全不同的。
const a = {
toString: function() { return 54321 },
};
const b = {
valueOf: function() { return 12345 },
};
String(a) // "54321"
b + '' // "12345"
.toString()
(123).toString() // "123"
+
、-
+('123') // 123
-('-123') // 123
這個方法有個缺點,就是很容易造成各種語意上的誤會,像是與遞增(++
)和遞減(--
)或與二元運算子的數學運算「加」(+
)和「減」(-
)混淆。
較常使用一元正和負運算子 +
、-
的時機是將 日期轉為數字,也就是取得 1970 年 1 月 1 日 00:00:00 UTC 到目前為止的毫秒數,或稱 UNIX 時間戳記、時戳值 timestamp。
const timestamp = +new Date();
timestamp // 1539236301262
經由強制轉型取得時戳值並不是很好的方法,建議改用 Date.now()
或 .getTime()
會是更理想的作法,可讀性更高。
~
位元否定運算子(bitwise not)的功能是進行二進位的補數(公式為 ~x
得到 -(x+1)
,例如:~42 得到 -43),它會先將值經由 ToNumber 轉為數字,再經由 ToInt32 轉為 32 位元有號整數,最後再逐位元的否定,很類似 !
強制將值轉為布林並反轉其真偽的運作方式。
範例如下,~
接受 indexOf 的回傳值並作轉換,對於「找不到」的 -1 會轉為 0,做條件判斷時會再轉為 false,其他因找而回傳的索引值(例如:0、1、2...)經否定再轉布林時都會是 true,這樣的寫法有助於提高可讀性。
const str = 'Hello World';
function find(target) {
const result = str.indexOf(target);
if (~result) {
console.log(`找到了,索引值原本是 ${result},被轉為 ${~result}`);
} else {
console.log(`找不到,回傳結果原本是 ${result},被轉為 ${~result}`);
}
}
find('llo'); // 找到了,索引值原本是 2,被轉為 -3
find('abc') // 找不到,回傳結果原本是 -1,被轉為 0
使用 ~~
將浮點數轉為整數,其運作方式為反轉兩次而得到截斷小數的結果,類似 !!
的真偽值雙次否定。
這裡有兩件事情要注意...
x | 0
也可以得到同樣的效果,差別只在於 ~~
運算子優先權較高,遇到四則運算時不用包小括號。Math.floor(..)
的結果不同。如下,Math.floor(-29.8)
得到 -30,而 ~~-29.8
得到 --29。Math.floor(-29.8) // -30
~~-29.8 // -29
-29.8 | 0 // -29
除了使用 Numer(..)
將值強制轉型為數字外,還可用 parseInt(..)
剖析而得到數字。parseInt(..)
的用途是將字串剖析為數字,它接受一個字串作為輸入,若輸入非字串的值則會使用 ToString 強制轉為字串。
Numer(..)
與 parseInt(..)
的差異在於
parseInt(..)
可容忍(或想像成忽略)非數值的字元,在由左至右掃描值的過程中,遇到非數值字元就停下來(忽略後續部份),只轉換到停下來之前所得到的數值。除非整個字串都是非數值,否則不會得到 NaN。而 Numer(..)
則是只要傳入的字串不是可轉成數值的,就會得到 NaN。parseInt(..)
若沒有輸入第二個參數來指定基數,就會以第一個參數的頭幾個字元決定基數為何,例如:開頭若為 0X
就會轉為十六進位的數字。因此,使用 parseInt(..)
最好要傳入基底以維持結果的正確性,例如:parseInt('12345', 10)
。var a = '123';
var b = '123px';
Number(a) // 123
parseInt(a) // 123
Number(b) // NaN
parseInt(b) // 123
parseInt('HelloWorld') // NaN
探討任何值強制轉為布林的情況。
Boolean(..)
使用 Boolean(..)
來執行 ToBoolean 的轉換工作。
Boolean('Hello World') // true
Boolean([]) // true
Boolean({}) // true
Boolean(null) // false
Boolean(undefined) // false
Boolean(NaN) // false
Boolean(0) // false
Boolean('') // false
!
雙次否定即可強制將值轉為布林。
!!'Hello World' // true
!![] // true
!!{} // true
!!null // false
!!undefined // false
!!NaN // false
!!0 // false
!!'' // false
「隱含的強制轉型」是指在程式碼中沒有明確指出要轉換型別但卻轉型的動作。
+
運算子是數字的相加,還是字串的串接?若兩運算元的型別不同,當其中一方是字串時,+
所代表的就是字串運算子,而會將另外一個運算元強制轉型為字串,並連接兩個字串。這裡提到的「另外一個運算元」就先稱它為 b 好了,若 b 是物件則會呼叫 ToPrimitive 做處理-由於傳入 [[DefaultValue]]
演算法的參數是 number,因此先使用 valueOf
取得基型值,然後再用 toString
轉為字串;若 b 是簡單的基本型別,則就會轉為 undefined
、null
、true
、false
或數字(非常大或非常小的數字以指數呈現)的字串格式。
如下範例,數字 1 會轉為字串 '1'
,而陣列 c 和 d 分別會使用 toString
轉為 '1, 2'
與 '3, 4'
。
const a = '1';
const b = 1;
const c = [1, 2];
const d = [3, 4];
a + 1 // "11"
b + 1 // 2
b + '' // "1"
c + d // "1,23,4"
再看兩個著名的例子:[] + {}
與 {} + []
。
先猜猜看結果是什麼?
皆為 [object Object]
?
...
...
...
公佈答案摟!
[] + {} // "[object Object]"
{} + [] // 0
說明如下
[] + {}
中,[]
會轉為空字串,而 {}
會轉為字串 "[object Object]"
。{} + []
中,{}
被當成空區塊而無作用, +[]
被當成強制轉型為數字 Number([])
(由於陣列是物件,中間會先使用 toString
轉成字空串,導致變成 Number('')
)而得到 0。注意,前面提到的 String(..)
是直接調用 .toString
來轉字串,與 +
字串運算子經過 ToPrimitive 的運作-由於傳入 [[DefaultValue]]
演算法的參數是 number,因此先使用 valueOf
取得基型值,然後再用 toString
轉為字串,兩種方法是完全不同的。
const a = {
toString: function() { return 54321 },
};
const b = {
valueOf: function() { return 12345 },
};
String(a) // "54321"
b + '' // "12345"
const a = '1';
a + 1 // "11"
a - 0 // 1
a * 1 // 1
a / 1 // 1
[9] - [7] // 2
轉換規則可參考前面提到的 ToNumber。
在什麼狀況下會隱含地將值強制轉為布林呢?
條件 ? 值1 : 值2
中的條件運算,意即測試運算式的第一個子句||
(or) 和 &&
(and)左手邊的運算元會被當成測試運算式轉換規則可參考前面提到的 ToBoolean。
範例如下。
var a = 12345;
var b = 'Hello World';
var c; // undefined
var d = null;
if (a) { // true
console.log('a 是真的'); // a 是真的
}
while (c) { // false
console.log('從來沒跑過');
}
c = d ? a : b;
c // "Hello World"
if ((a && d) || c) { // true
console.log('結果是真的'); // 結果是真的
}
||
與 &&
邏輯運算子的 ||
(or) 和 &&
(and) 其實應該要稱呼為「(運算元的)選擇器運算子」(operand selector operator),這是因為它們並不是產生邏輯運算值 true 或 false,而是在兩個運算元當中「選擇」其中一個運算元的值作為結果。
規則為,||
(or) 和 &&
(and)會將第一個運算元做布林測試或強制轉型為布林以便測試。
||
(or)來說,若結果為 true,則取第一個運算元為結果;若結果為 false,則取第二個運算元為結果。&&
(and)來說,若結果為 true,則取第二個運算元為結果;若結果為 false,則取第一個運算元為結果。因此可應用於
||
(or) 可用來設定變數的初始值。&&
(and)可用來執行「若特定條件成立,才做某件事情」,功能近似 if 述句。範例如下。
const a = 'Hello World!'
const b = 777;
const c = null;
a && c // 測試 a 為 true,選 c,結果是 null
a && b // 測試 a 為 true,選 b,結果是 777
undefined && b // 測試 undefined 為 false,選 undefined,結果是 undefined
a || b // 測試 a 為 true,選 a,結果是 "Hello World!"
c || 'foo' // 測試 c 為 false,選 'foo',結果是 "foo"
若 flag 條件成立(true),就執行函式 foo,之後會再提到短路的議題。
const flag = true;
function foo() {
console.log('try me');
}
flag && foo(); // try me
symbol 的強制轉型規則如下
var s1 = Symbol('Hello World');
String(s1); // "Symbol(Hello World)"
var s2 = Symbol(' World Hello');
s2 + ''; // TypeError: Cannot convert a Symbol value to a string
const n1 = Symbol(777);
Number(s1); // TypeError: Cannot convert a Symbol value to a number
const n2 = Symbol(999);
+n2; // TypeError: Cannot convert a Symbol value to a number
const b1 = Symbol(true);
const b2 = Symbol(false);
Boolean(b1); // true
Boolean(b2); // true
const b3 = Symbol(true);
const b4 = Symbol(false);
if (b3) {
console.log('b3 是真的');
}
if (b4) {
console.log('b4 是真的');
}
// b3 是真的
// b4 是真的
關於相等性的運算子有四個「==
」(寬鬆相等性 loose equality)、「===
」(嚴格相等性 strict equality)、「!=
」(寬鬆不相等 loose not-equality)和「!==
」(嚴格不相等 strict not-equality)。寬鬆與嚴格的差異在於檢查值相等時,是否會做 強制轉型,==
會做強制轉型,而 ===
不會。
const a = '100';
const b = 100;
a == b // true,強制轉型,將字串 '100' 轉為數字 100
a === b // false
這裡要說明一下,==
和 ===
其實都會做型別的檢查,只是當面對型別不同時的反應是不一樣的而已。
如果型別相同,就會以同一性做比較,但要注意
如果型別不同,則會先將其中一個或兩個值先做強制轉型(可遞迴),再用型別相同的同一性做比較。
valueOf()
(優先)或 toString()
將物件取得基本型別的值,再做比較。而 !=
和 !==
就是先分別做 ==
和 ===
再取否定(!
)即可。
可參考規格。
const a = '123';
const b = 123;
a === b // 答案是?
a == b // 答案是?
...
...
...
答案揭曉。
a === b // false
a == b // true
在 a == b
當中,字串 a 優先轉為數字後,此時就可比較 123 == 123
,因此是相等的(true)。
const a = true;
const b = 123;
a === b // 答案是?
a == b // 答案是?
...
...
...
答案揭曉。
a === b // false
a == b // false
在 a == b
當中,布林 a 優先轉為數字(Numer(true)
得到 1)後,此時就可比較 1 == 123
,因此是不相等的(false)。
const a = null;
const b = 123;
a === b // 答案是?
a == b // 答案是?
...
...
...
答案揭曉。
a === b // false
a == b // false
在 a == b
當中其實比較的是 null == 123
,因此是不相等的(false)。
const a = '1,2,3';
const b = [1,2,3];
a === b // 答案是?
a == b // 答案是?
...
...
...
答案揭曉。
a === b // false
a == b // true
在 a == b
當中,陣列 a 由於沒有 valueOf()
,只好使用 toString()
取得其基型值而得到字串 '1,2,3'
,此時就可比較 '1,2,3' == '1,2,3'
,因此是相等的(true)。
有幾個例外需要注意...
Object(null)
與 Object(undefiend)
等同於 Object()
,也就是空物件 {}
。Number(NaN)
得到 NaN,且 NaN 不等於自己。範例如下。
var a = null;
var b = Object(a); // 等同於 Object()
a == b; // false
var c = undefined;
var d = Object(c); // 等同於 Object()
c == d; // false
var e = NaN;
var f = Object(e); // 等同於 new Number(e)
e == f;
這部份來提一些邊緣(少見但驚人)的狀況。
valueOf(..)
經由原生的內建函式所建立的值,由於是物件型態,在強制轉型時會經過 ToPrimitive
的過程,也就是使用 valueOf(..)
(優先)或 toString(..)
將物件取得基本型別的值,才會做後續比較。因此,若修改了原型中的 toValue(..)
方法,則可能會導致比較時出現「不可思議」的結果。
Number.prototype.valueOf = function() {
return 3;
};
new Number(2) == 3; // true
以下會得到什麼結果呢?請小心服用。
"0" == false;
false == 0;
false == "";
false == [];
false == {};
"" == 0;
"" == [];
"" == {};
0 == [];
0 == {};
[] == ![]
2 == [2]
"" == [null]
0 == '\n'
...
...
...
答案揭曉。
...
...
...
說明
"0" == false;
,true,字串轉數字、布林再轉數字false == 0;
,true,布林轉數字false == "";
,true,字串轉數字、布林再轉數字false == [];
,true,布林轉數字、陣列取 toString 得到空字串再轉數字false == {};
, false,布林轉數字、物件取 valueOf 得到空物件"" == 0;
,true,字串轉數字"" == [];
,true,字串轉數字、陣列取 toString 得到空字串再轉數字"" == {};
,false,字串轉數字、物件取 valueOf 得到空物件0 == [];
,true,陣列取 toString 得到空字串再轉數字0 == {};
,false,物件取 valueOf 得到空物件[] == ![]
,true,左手邊取 valueOf 得到空字串再轉數字得到 0,右手邊被 ! 強制轉為布林得到 false 再轉為數字2 == [2]
,true,陣列取 toString 得到空字串再轉數字"" == [null]
,true,陣列取 toString 得到空字串,轉數字後得到 00 == '\n'
,true,'\n' 意即 ' '(空白),轉數字後得到 0若允許強制轉型,但又希望能避免「難以預料」的強制轉型(上例),這裡有一些建議...
==
,改用 ===
。[]
、空字串 ""
或 0 ,就不要用 ==
,改用 ===
。以下是一定很安全的強制轉型,使用 ==
即可,不需要用 ===
...
typeof x
得到的是固定的七種字串值(例如:'string'
、'number'
、'boolean'
、'undefined'
、'function'
、'object'
、'symbol'
),因此做 typeof x == '指定值'
一定是安全的。...
...
也許世界上大多數的開發者都詬病 JavaScript 中「隱含的強制轉型」的這部份,覺得這是個壞東西,但也許它其實是減少冗贅、反覆套用和非必要實作細節的好方法,而前提是,必須要能清楚了解強制轉型的規則。
...
...
下圖為 JavaScript 中的相等性,此圖視覺化了所有的比較項目。
圖片來源:JavaScript Equality Table
這裡要來談比較運算子(comparison)的部份,意即 <
(小於)、 >
(大於)、<=
(小於等於)、>=
(大於等於),例如:a > b
表示比較 a 是否大於 b。
其比較規則為
valueOf
取得基型值,然後再用 toString
方法轉為字串。注意
a < b
的演算法,因此 a > b
會以 b < a
的方式做比較。範例如下,由於 a 和 b 都不是字串且陣列沒有 valueOf
,因此先用 toString
取得基型值,得到 a 為 '12'
、b 為 '13'
,型別都是字串,接著做字母順序的比較。
const a = [12];
const b = ['13'];
a < b // true,'12' < '13'
a > b // false,其實是比較 b < a,即 '13' < '12'
範例如下,由於 a 和 b 都不是字串,因此先用 valueOf
取得基型值(只取到原來的物件),再用 toString
而得到兩個字串 [object Object]
,因此比較 [object Object]
與 [object Object]
。又,a == b
比較的是兩物件存值的所在的記憶體位置,也就是參考(reference)。
const a = { b: 12 };
const b = { b: 13 };
a < b // false,'[object Object]' < '[object Object]'
a > b // false,其實是比較 b < a,即 '[object Object]' < '[object Object]'
a == b // false,其實是比較兩物件的 reference
a >= b // true
a <= b // true
這裡要注意的是...
a <= b
其實是 !(b > a)
,因此 !false
得到 true。a >= b
其實是 b <= a
也就是 !(a > b)
等同於 !false
得到 true。看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到...
同步發表於部落格。
恭喜讀完「導讀,型別與文法」最困難的部份「強制轉型」!明天見 (*´∀`)~♥
勘誤(不影響內容)
a <= b 其實是 !(b > a),因此 !false 得到 true。
a >= b 其實是 b <= a 也就是 !(a > b) 等同於 !false 得到 true。
把物件改成 Number 會發現不對。
let a=1, b=10;
console.log(a<= b === !(a > b)); // true
console.log(a<= b === !(b > a)); // false
在看到文章之前,我以為自己弄懂了,現在越看越迷糊
先謝謝前輩分享寶貴知識,在看玩文章後有些問題希望前輩解答。
問題一 抽象的值運算
在這個小結中,介紹了ToString、ToBoolean、ToNumber,裡面也說明了object轉基本型態的規則,那麼ToPrimitive的作用又是什麼呢?它也定義了object隱含轉換的規則,我們應該使用哪個規則去轉換object?
問題二 ToPrimitive
裡面對object轉換規則的說明如下:
object:使用 [[DefaultValue]] 內部方法,依照傳入的參數來決定要使用 toString或valueOf取得基本型別值。
要怎麼看出傳入DefaultValue內部方法的參數值(number,string)呢,看了英文的介紹還是不太懂。 我本來是認為是依運算式之間的關係來做轉換,比如以下例子我會認為它會將obj轉為數字後加1 ,控制台打印21。
```
var obj = {};
obj.toString = function(){return '10';};
obj.valueOf = function(){return '20'};
//我會認為它會把obj呼叫valueOf的返回值轉為數字後將加
console.log(obj + 1); // '201'
//我會認為它會把obj呼叫toString的返回值轉為字串做串接
console.log(obj + '1') // '201'
```
可是實驗結果不是,而且只要object有定義valueOf方法就會以它為優先,只有當valueOf返回的不是基本型態才會再去呼叫toString方法取得值。